package metaballs;

import java.awt.Dimension;
import java.nio.FloatBuffer;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

import javax.media.opengl.GL;

import util.BasicGraphicsJogl;
import util.Vec2F;
import util.Vec2I;
import util.shaders.Program;
import util.texture.StockTextureFactory;
import util.texture.TextureDestinationWrapper;
import com.sun.opengl.util.BufferUtil;
import com.sun.opengl.util.texture.Texture;

public class MetaballSystem
{
	static int frame = 0;
	private Program accumulateMetaballProgram;
	private Program clampMetaballProgram;
	private Program renderMetaballProgram;
	private Program sumMetaballProgram;
	
	private float mass;
	private int maxBalls;
	private MetaBallTexture metaballTexture;
	private Texture colorTexture;
	private final Texture accumulationTexture;
	private final TextureDestinationWrapper accumulationDestination;
	private final Set<Metaball> metaballs;
	private final LinkedList<MetaballClump> metaballClumps;
	FloatBuffer vertexBuffer;
	FloatBuffer textureBuffer;
	FloatBuffer massBuffer;
	
	float minRepulsion=10,maxRepulsion=200,repulsionRadius=64;
	
	public MetaballSystem(int maxBalls,float mass,MetaBallTexture metaballTexture,Texture colorTexture,Dimension renderDims)
	{
		this.mass = mass;
		this.maxBalls = maxBalls;
		this.metaballTexture = metaballTexture;
		this.colorTexture = colorTexture;
		accumulationTexture = StockTextureFactory.createFloatingPointTexture(renderDims, true, true);
		accumulationDestination = new TextureDestinationWrapper(accumulationTexture);

		
		metaballs = new HashSet<Metaball>();
		metaballClumps = new LinkedList<MetaballClump>();
		MetaballClump metaballClump = new MetaballClump(new Vec2F(256,256), 100, 30, 50);
		metaballClump.initializeStableClump(50);
		
		addClump(metaballClump);
		allocate();
	}
	public void addClump(MetaballClump metaballClump)
	{
		metaballClumps.add(metaballClump);
		add(metaballClump.getMetaballs());
	}
	public void add(Metaball metaball)
	{
		metaballs.add(metaball);
	}
	public void add(LinkedList<Metaball> newMetaballs)
	{
		metaballs.addAll(newMetaballs);
	}
	public void destroy()
	{
		accumulateMetaballProgram.release();
		clampMetaballProgram.release();
		sumMetaballProgram.release();
		
	}
	public void allocate()
	{
		accumulateMetaballProgram = Program.createProgram("/resources/shaders/vertex_accumulate_metaballs.glsl","/resources/shaders/fragment_accumulate_metaballs.glsl");
		clampMetaballProgram = Program.createProgram("/resources/shaders/vertex_texture.glsl","/resources/shaders/fragment_clamp_metaballs.glsl");
		renderMetaballProgram = Program.createProgram("/resources/shaders/vertex_render_metaballs.glsl","/resources/shaders/fragment_render_metaballs_texture.glsl");
		sumMetaballProgram = Program.createProgram("/resources/shaders/vertex_texture.glsl","/resources/shaders/fragment_reduce_sum_4x.glsl");
		
		//2 floats per vertex, 4 verices per ball
		vertexBuffer = BufferUtil.newFloatBuffer(maxBalls * 2 * 4);
		
		//2 floats per vertex, 4 verices per ball
		textureBuffer = BufferUtil.newFloatBuffer(maxBalls * 2 * 4);
		 
		//1 floats per vertex, 4 verices per ball
		massBuffer = BufferUtil.newFloatBuffer(maxBalls * 1 * 4);
		
		//Fill in texture buffer for max metaballs (will always be the same).
		for(int i=0;i<maxBalls;i++)
		{
			textureBuffer.put(0f);
			textureBuffer.put(0f);
			
			textureBuffer.put(1f);
			textureBuffer.put(0f);
			
			textureBuffer.put(1f);
			textureBuffer.put(1f);
			
			textureBuffer.put(0f);
			textureBuffer.put(1f);
			
		}
	}
	
	public void applyForces()
	{
		Vec2F displacementVector,displacementUnitVector;
		float displacementSquared;
		float repulsionRadiusSquared = repulsionRadius * repulsionRadius;
		float force;
		
		Object[] metaballArray = metaballs.toArray();
		
		Metaball m1,m2;
		
		for(int i = 0;i<metaballArray.length - 1;i++)
		{
			m1 = (Metaball)metaballArray[i];
			for(int j = i + 1;j< metaballArray.length; j ++)
			{
				m2 = (Metaball)metaballArray[j];
				displacementVector = m2.getLocation().sub(m1.getLocation());
				displacementSquared = displacementVector.mag2();
				if(displacementSquared >= repulsionRadiusSquared)
				{
					break; 
				}
				force = minRepulsion + (maxRepulsion - minRepulsion) * (repulsionRadiusSquared - displacementSquared) / repulsionRadiusSquared;
				displacementUnitVector = displacementVector.unit();
				m1.accumulateAcceleration(displacementUnitVector.scale(-force));
				m2.accumulateAcceleration(displacementUnitVector.scale(force));
			}
		}
		for(Metaball metaball:metaballs)
		{
			metaball.accumulateAcceleration(metaball.getVelocity().scale(-0.01f * metaball.getVelocity().mag()));
		}
	}
	public void update(float time)
	{
		applyForces();
		for(MetaballClump metaballClump: metaballClumps)
		{
			metaballClump.applyForces();
		}

//		if(frame % 40 == 0)
//		{
//			Random random = new Random();
//			Vec2F explosionCenter = new Vec2F(random.nextFloat()*512,random.nextFloat()*512);
//			for(Metaball metaball: metaballs)
//			{
//				float displacement;
//				Vec2F displacementVector = metaball.getLocation().sub(explosionCenter);
//				displacement = displacementVector.mag();
//				metaball.accumulateAcceleration(displacementVector.scale(3000f/displacement));
//			}
//		}
		
		for(Metaball metaball: metaballs)
		{
			metaball.update(time);
		}
//		Object[] ms = metaballs.toArray();
//		Metaball m = (Metaball) ms[0];
//		m.setLocation(new Vec2F(128,128));
//		m = (Metaball) ms[1];
//		m.setLocation(new Vec2F(300f,300.49f));
		
//		metaballClumps.get(0).setCenter(metaballClumps.get(0).getCenter().add(new Vec2F(1f,0.3f)));
//		metaballClumps.get(0).setRadius(metaballClumps.get(0).getRadius()+10);
		
		frame++;
	}
	
	/*
	 * Calculates the total accumulated metaball mass after clamping etc.
	 */
	public float calcAccumulatedMass()
	{
		float accumulatedMass = 0.0f;
		for(Metaball metaball: metaballs)
		{
			accumulatedMass += metaball.getMass() * metaballTexture.getIntegral();
		}
		return accumulatedMass;
	}
	
	public void render(Vec2F location)
	{
		BasicGraphicsJogl basicGraphics = BasicGraphicsJogl.instance();
		GL gl = basicGraphics.getGL();
		
		basicGraphics.setRenderTarget(accumulationDestination);
		basicGraphics.setShaderProgram(accumulateMetaballProgram);
		gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
		gl.glBlendFunc (GL.GL_SRC_ALPHA, GL.GL_ONE);
		
		
		gl.glClear(GL.GL_COLOR_BUFFER_BIT);// | GL.GL_DEPTH_BUFFER_BIT);
		
		gl.glVertexAttrib1f(accumulateMetaballProgram.getAttributeLocation("mass"), 1.0f);
		for(Metaball metaball: metaballs)
		{
			basicGraphics.drawCenteredImage(metaballTexture.getTexture(), metaball.getLocation());
		}
		
//		gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
//		gl.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY);
//		gl.glEnableVertexAttribArray(accumulateMetaballProgram.getAttributeLocation("mass"));
		
//		vertexBuffer.clear();
//		massBuffer.clear();
//		for(Metaball metaball: metaballs)
//		{
//			metaball.appendRenderBuffers(vertexBuffer, massBuffer);
//		}
//		vertexBuffer.rewind();
//		textureBuffer.rewind();
//		massBuffer.rewind();
//		
//		gl.glVertexPointer(2, GL.GL_FLOAT, 0, vertexBuffer);
//		gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, textureBuffer);
//		gl.glVertexAttribPointer(accumulateMetaballProgram.getAttributeLocation("mass"), 1, GL.GL_FLOAT, false, 0, massBuffer);
//		metaballTexture.bind();
//		gl.glDrawArrays(GL.GL_QUADS,0,metaballs.size()*4);
//		gl.glFlush();
		
		
		
//		gl.glDisableVertexAttribArray(accumulateMetaballProgram.getAttributeLocation("mass"));
//		gl.glDisableClientState(GL.GL_TEXTURE_COORD_ARRAY);
//		gl.glDisableClientState(GL.GL_VERTEX_ARRAY);

		//Restore clearing method state
		gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		gl.glBlendFunc (GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
		
		basicGraphics.clearRenderTarget();
		
		basicGraphics.setShaderProgram(renderMetaballProgram);
		float accumulatedMass = calcAccumulatedMass();
		
		renderMetaballProgram.setUniformFloat("massScaleFactor",mass/accumulatedMass);
		renderMetaballProgram.setTexture("accumulationTexture", accumulationTexture,0);
		renderMetaballProgram.setTexture("mappingTexture", colorTexture,1);
		
//        basicGraphics.drawCenteredImage(accumulationTexture, location);
		basicGraphics.drawCenteredQuadWithTextureCoords(location,new Vec2F(accumulationTexture.getWidth(),accumulationTexture.getHeight()));	
        basicGraphics.setShaderProgram(null);
		
//		basicGraphics.setShaderProgram(metaballSumProgramHandle);
//		basicGraphics.switchRenderTarget(sumDestination);
//		gl.glClear(GL.GL_COLOR_BUFFER_BIT);// | GL.GL_DEPTH_BUFFER_BIT);
//		basicGraphics.drawImage(renderTexture, new Vec2F(0,0), new Vec2F(renderdimension.width >> 2,renderdimension.height >> 2));

		//basicGraphics.setShaderProgram(clampMetaballProgram);
		//basicGraphics.drawCenteredImage(accumulationTexture, new Vec2F(256,256));
		//basicGraphics.drawCenteredImage(sumTexture, location);

	}
}
